/* =============================================================================
 * CS544 - Computer Networks
 * Drexel University, Spring 2013
 * Protocol Implementation: Remote Smart House Control
 * Group 12:
 * - Ryan Corcoran
 * - Amber Heilman
 * - Michael Mersic
 * - Ariel Stolerman
 * 
 * -----------------------------------------------------------------------------
 * File name: ClientCommCLI.java
 * 
 * Purpose:
 * Provides an implementation of ClientComm (see ClientComm.java), which handles
 * client communication to the server. This class collects user input, handles
 * message parsing and generation.
 * Optional commands are displayed conveniently to the user, and the raw message
 * input and output are also displayed for "inner" look at the protocol (however
 * the user does not have to know anything about the protocol; all user I/O is
 * handled with user-friendly messages).
 * 
 * Relevant requirements (details in the file):
 * - CLIENT
 * - UI
 * 
 * =============================================================================
 */

package client;

import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketTimeoutException;

import protocol.*;

import common.Util;

public class ClientCommCLI implements ClientComm {

	// indicator to flag the client to process user input
	private static final String POSTED_MESSAGE = "POSTED_MESSAGE";
	
	// fields
	
	/**
	 * Host to connect to
	 */
	private String host;
	/**
	 * Port to connect to
	 */
	private int port;
	/**
	 * Username
	 */
	private String user;
	/**
	 * Password
	 */
	private String pass;
	/**
	 * DFA to be used to track protocol states and process messages
	 */
	private DFA dfa;
	/**
	 * Thread that handles user I/O
	 */
	private ClientInputThread clientInputThread;
	/**
	 * Holds messages posted by the user I/O thread, whenever an input is
	 * received from the user
	 */
	private volatile Message postedAction;
	
	// constructors
	
	/**
	 * Constructor for a client communication handler with a CLI for processing
	 * client input.
	 * @param host the host to connect to.
	 * @param port the port to connect to.
	 * @param user client username.
	 * @param pass client password.
	 */
	public ClientCommCLI(String host, int port, String user, String pass) {
		this.host = host;
		this.port = port;
		this.user = user;
		this.pass = pass;
		this.dfa = new ClientDFA(this, user, pass);
	}
	
	// methods
	
	/*
	 * CLIENT
	 * main thread to handle client connection to the server and user input
	 */
	
	@Override
	public void run() {
		boolean userShutDown = false;
		try {
			// initialize socket
			Socket socket = new Socket(host, port);
			socket.setSoTimeout(Client.LISTEN_TIMEOUT_MS);

			System.out.println(Util.dateTime() + " -- Client " + user
					+ " connected\n");
			
			BufferedReader br = new BufferedReader(
					new InputStreamReader(socket.getInputStream()));
			BufferedWriter bw = new BufferedWriter(
					new OutputStreamWriter(socket.getOutputStream()));
			
			// poke
			write(dfa.process(Message.INTERNAL), bw);
			
			while (true) {
				// read next message from input
				String line = read(socket, br);
				if (line == null) return;
				
				// process input message
				// ---------------------
				Message inMsg;
				// message generated by user input
				if (line == POSTED_MESSAGE) {
					inMsg = postedAction;
					//postedAction = null;
				}
				// message received from server
				else {
					inMsg = Message.fromHexString(line);
					inMsg.prettyPrint("S");
					
					// handle shutdown
					if (inMsg.opcode() == Message.OP_SHUTDOWN ||
							inMsg.opcode() == Message.OP_ERROR) {
						clientInputThread.killInput();
						break;
					}
				}
				
				// process output message
				// ---------------------
				Message outMsg = dfa.process(inMsg);
				// error
				if (outMsg == null) {
					throw new RuntimeException("Invalid Out Message.");
				}
				// collect input from user
				else if (outMsg == Message.AWAITING_CLIENT_INPUT) {
					/*
					 * UI
					 * start a client input thread
					 */
					createClientInputThread();
				}
				// send message to server
				else {
					write(outMsg, bw);
					// shutdown if user selected to shutdown
					if (outMsg == Message.SHUTDOWN) {
						userShutDown = true;
						break;
					}
				}					
			}
			
			// shutdown
			socket.close();
			System.out.println(Util.dateTime() + " -- Client " + user + " disconnected");
			if (!userShutDown) System.out.println("Press any key to exit");
			
		}
		catch (ConnectException ce) {
			System.out.println("Unable to connect to " + host + ":" + port);
			System.out.println("Make sure RSHC server is running and try again");
		}
		catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Initializes the client input thread, that runs in parallel to the client
	 * listening on server updates.
	 */
	private void createClientInputThread() {
		/*
		 * UI
		 * initialize client input thread
		 */
		clientInputThread = new ClientInputThread(this, this.dfa.house());
		clientInputThread.start();
	}
	
	/**
	 * Utility method to read from the input buffer.
	 */
	private String read(Socket socket, BufferedReader br) throws IOException {
		String line = null;
		while (line == null) {
			try {
				// read message from server
				line = br.readLine();
				if (line == null) {
					// server closed connection
					socket.close();
					return null;
				}
			} catch (SocketTimeoutException ste) {
				// if user generated a posted message, flag to parse that message
				if (this.postedAction != null) {
					return POSTED_MESSAGE;
				}
			}
		}
		return line;
	}

	/**
	 * Utility method to write to the output buffer.
	 */
	private void write(Message m, BufferedWriter bw) throws IOException {
		m.prettyPrint("C");
		m.write(bw);
	}
	
	// getters
	
	public String host() {
		return host;
	}
	
	public int port() {
		return port;
	}
	
	public String user() {
		return user;
	}
	
	public String pass() {
		return pass;
	}
	
	// overriding
	
	@Override
	public void postAction(Message actionMessage) {
		this.postedAction = actionMessage;
	}
	
	@Override
	public Message getPostedActionAndReset() {
		Message res = postedAction;
		postedAction = null;
		return res;
	}
	
	@Override
	public void killInput() {
		this.clientInputThread.killInput();
	}
}